/*
 * com.micheldalal.x10 library.  Set of X10 Home Control classes.
 *
 * Copyright (C) 2001-2003  Michel Dalal.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * You may also find a copy of the licence at
 * <a href="http://www.gnu.org/copyleft/lesser.html">http://www.gnu.org/copyleft/lesser.html</a>
 */

package com.micheldalal.x10;

/**
 * Copyright (C) 2001-2003 Michel Dalal
 */

import gnu.io.SerialPort;

/**
 * CM17A is the FireCracker Computer Interface shipped with
 * <a href="http://www.x10.com/firecracker/fc_kitcomponents_br1ab.htm">The FireCracker Ultimate Home Control Kit</a>.
 * This class uses the Java Communications API (COMM) to communicate via the CM17A with the
 * Wireless Link Module(TM) (a.k.a. Transiever Module).
 * <p>
 * This is an implementation of the
 * <a href="ftp://ftp.x10.com/pub/manuals/cm17a_protocol.txt">FireCracker (CM17A) Communications Specification</a>.
 * The basics of the interface are:
 * <pre>
 *
 *        Standby | '1' | Wait | '0' | Wait | '1' | Wait...
 *        _____________________       _____________________
 * RTS  _|                     |_____|
 *        ________       ___________________       ________
 * DTR  _|        |_____|                   |_____|
 * </pre>
 * <p>
 * The recommended time for the '1', '0' and 'Wait' states is at
 * least 0.5ms.
 * <p>
 * Each transmission consists of 40 bits. This is made up of
 * the following:
 * <pre>
 * 	Header		16 bits
 * 	Data		16 bits
 * 	Footer		8 bits
 * </pre>
 * <p>
 * The header and footer never change and are defined as follows:
 * <pre>
 * 	Header		11010101 10101010
 * 	Footer		10101101
 * </pre>
 * <p>
 * The following table defines the states of the RTS and DTR signals
 * required to send a logical '1' and logical '0' to the interface:
 * <pre>
 * 	Signal			RTS	DTR
 * 	Reset			0	0
 * 	Logical '1'		1	0
 * 	Logical '0'		0	1
 * 	Standby			1	1
 * </pre>
 *
 * @author Michel Dalal
 * @version 1.0 07/05/2001: Initial Version.
 * @version <br>1.0.1 10/20/2003: Comment updates
 */
public class CM17A
{
	/**
	 * The first data byte.
	 * <ol>
	 *	<li>Specifies the house code.
	 *	<li>A mask may be applied to specify the unit id's from 9 to 16.
	 * </ol>
	 */
	private final static int []FIRST_DATA_BYTE=
	{
		0x60,		// A
		0x70,		// B
		0x40,		// C
		0x50,		// D
		0x80,		// E
		0x90,		// F
		0xA0,		// G
		0xB0,		// H
		0xE0,		// I
		0xF0,		// J
		0xC0,		// K
		0xD0,		// L
		0x00,		// M
		0x10,		// N
		0x20,		// O
		0x30		// P
	};

	/**
	 * The seconds data byte.
	 * <ol>
	 *	<li>Specifies the unit id.
	 * </ol>
	 */
	private final static int []SECOND_DATA_BYTE=
	{
		0x00,		// 1 & 9
		0x10,		// 2 & 10
		0x08,		// 3 & 11
		0x18,		// 4 & 12
		0x40,		// 5 & 13
		0x50,		// 6 & 14
		0x48,		// 7 & 15
		0x58,		// 8 & 16
	};
	
	/** The first data byte adjustment for unit id's from 9 to 16. */
	private final static int FIRST_UPPER_UNIT_ID=0x40;
	
	/** The second data byte adjustment when turning off a unit. */
	private final static int SECOND_UNIT_ID_OFF=0x20;

	/** Brighten by 5% command. */
	private final static int SECOND_BRIGHTEN_5_PERCENT=0x88;	

	/** Dim by 5% command. */
	private final static int SECOND_DIM_5_PERCENT=0x98;

	/** The pulse standby and wait time is at least 0.5ms. */
	private final static int DELAY_TIME=1;

	/** The first byte of the standard header. */
	private final static int STANDARD_HEADER_1=0xD5;
	
	/** The second byte of the standard header. */
	private final static int STANDARD_HEADER_2=0xAA;
	
	/** The second byte of the standard header. */
	private final static int STANDARD_FOOTER=0xAD;
	
	/** The reference to the serial port. */
	private SerialPort serialPort;
	
	/**
	 * Creates this object and maintains a reference to the serial port.
	 * The serial port must be set prior to use.
	 */
	public CM17A()
	{
		this.serialPort=null;
	}
	
	/**
	 * Creates this object and maintains a reference to the serial port.
	 */
	public CM17A(SerialPort serialPort)
	{
		this.serialPort=null;
		setSerialPort(serialPort);
	}
	
	/**
	 * Sets the serial port used for communication.
	 * Also resets the attached CM17A device if the supplied serialPort is not null.
	 *
	 * @param serialPort the serial port object.
	 */
	public synchronized void setSerialPort(SerialPort serialPort)
	{
		this.serialPort=serialPort;
		if (serialPort!=null)
		{
			reset();
			delay();
			init();
		}
	}

	/**
	 * Returns the serial port used for communication.
	 */
	public synchronized SerialPort getSerialPort()
	{
		return serialPort;
	}

	/**
	 * Internal sleep mechanism needed for pulse delays.
	 */	
	private void delay()
	{
		try
		{
			Thread.sleep(DELAY_TIME);
		}
		catch (InterruptedException e)
		{
			System.err.println("X");
		}
	}
	
	/**
	 * Initializes the CM17A for use.
	 */
	private void init()
	{
		serialPort.setRTS(true);
		serialPort.setDTR(true);
	}

	/**
	 * Resets the CM17A.
	 */
	private void reset()
	{
		serialPort.setRTS(false);
		serialPort.setDTR(false);
	}
	
	/**
	 * Sends out a logical pulse.
	 *
	 * <pre>
	 * Logical Pulse: | '0'   | '1'   |
	 * 	              +-------+-------+
	 * RTS State:     | False | True  |
	 * 	              +-------+-------+
	 * DTR State:     | True  | False |
	 * 	              +-------+-------+
	 * </pre>
	 * For a '0', the RTS must be set to 0.
	 * For a '1', the DRT must be set to 0.
	 *
	 * @param state true means send a 1 and false means send a 0.
	 */
	private void sendBit(boolean state)
	{
		// Make sure in init mode.
		if (!serialPort.isRTS()||!serialPort.isDTR())
		{
			init();
		}
		delay();
		if (state)
		{
			// Send a logical '1'
			serialPort.setDTR(false);
		}
		else
		{
			// Send a logical '0'
			serialPort.setRTS(false);
		}
		delay();
		init();
	}

	/**
	 * Sends out the supplied byte pattern.
	 *
	 * @param pattern the byte pattern to send out.
	 */
	private void sendByte(int pattern)
	{
		sendBit((pattern&0x80)==0x80);
		sendBit((pattern&0x40)==0x40);
		sendBit((pattern&0x20)==0x20);
		sendBit((pattern&0x10)==0x10);
		sendBit((pattern&0x08)==0x08);
		sendBit((pattern&0x04)==0x04);
		sendBit((pattern&0x02)==0x02);
		sendBit((pattern&0x01)==0x01);
	}
	
	/**
	 * Sends out the standard header sequence.
	 */
	private void sendHeader()
	{
		sendByte(STANDARD_HEADER_1);
		sendByte(STANDARD_HEADER_2);
	}

	/**
	 * Sends out the standard footer sequence.
	 */
	private void sendFooter()
	{
		sendByte(STANDARD_FOOTER);
	}

	/**
	 * Sends out a specific command to the transiever module.
	 *
	 * @param firstByte the first byte of the command.
	 * @param secondByte the second byte of the command.
	 */
	private void sendCommand(int firstByte,int secondByte)
	{
		sendHeader();
		sendByte(firstByte);
		sendByte(secondByte);
		sendFooter();
	}
	
	/**
	 * Turns off/on the specified of the selected house.
	 *
	 * @param house the house to control.  'A' to 'P'.
	 * @param unit the unit to control. 1 to 16.
	 * @param state true to turn on and false to turn off.
	 * @exception IllegalArgumentException thrown if the house or unit are within the allowable range.
	 */
	public synchronized void setState(char house,int unit,boolean state) throws IllegalArgumentException
	{
		int firstByte,secondByte;
		
		house=Character.toUpperCase(house);
		if (house>='A'&&house<='P')
		{
			if (unit>=1&&unit<=16)
			{
				// For easy indexing, adjust the unit id down.
				unit--;
				firstByte=FIRST_DATA_BYTE[house-'A']|(unit>=8?FIRST_UPPER_UNIT_ID:0);
				secondByte=SECOND_DATA_BYTE[unit%8]|(state?0:SECOND_UNIT_ID_OFF);
				sendCommand(firstByte,secondByte);
			}
			else
			{
				throw new IllegalArgumentException("Invalid unit. "+unit);
			}
		}
		else
		{
			throw new IllegalArgumentException("Invalid house. "+house);
		}
	}
	
	/**
	 * Dims/brightens the lamp modules in the specified house.
	 *
	 * @param house the house to control.  'A' to 'P'.
	 * @param mode true to brighten and false to dim.
	 * @param percent the percentage to change.  Changes will occur is 5% increments.
	 * @exception IllegalArgumentException thrown if the house or unit are within the allowable range.
	 */
	public synchronized void adjustBrightness(char house,boolean mode,int percent) throws IllegalArgumentException
	{
		int firstByte,secondByte;
		int steps;
		
		house=Character.toUpperCase(house);
		if (house>='A'&&house<='P')
		{
			if (percent>=5&&percent<=100)
			{
				steps=percent/5;
		
				firstByte=FIRST_DATA_BYTE[house-'A'];
				secondByte=(mode?SECOND_BRIGHTEN_5_PERCENT:SECOND_DIM_5_PERCENT);
				while (steps-->0)
				{
					sendCommand(firstByte,secondByte);
				}
			}
			else
			{
				throw new IllegalArgumentException("Invalid percent. "+percent);
			}
		}
		else
		{
			throw new IllegalArgumentException("Invalid house. "+house);
		}
	}
	
	/**
	 * Finalizes this object.
	 */
	protected void finalize() throws Throwable
	{
		try
		{
			serialPort.close();
		}
		catch (Exception e)
		{
		}
	}
}
